Keyfiye CPI
Özet
- Bir CPI oluşturmak için hedef program, çağırma talimatı işleyicisine bir hesap olarak geçirilmelidir. Bu, herhangi bir hedef programın talimat işleyicisine geçirilebileceği anlamına gelir. Programınız, hatalı veya beklenmedik programları kontrol etmelidir.
- Yerel programlarda, geçilen programın genel anahtarını beklediğiniz programla basitçe karşılaştırarak kontrol edin.
- Bir program Anchor ile yazılmışsa, bu programın kamuya açık bir CPI modülü olabilir. Bu, başka bir Anchor programından programı çağırmayı basit ve güvenli hale getirir. Anchor CPI modülü, geçirilen programın adresinin modülde saklanan programın adresiyle eşleştiğini otomatik olarak kontrol eder.
Ders
Bir çapraz program çağrısı (CPI), bir programın başka bir programda bir talimat işleyicisini çağırmasıdır. "Rastgele CPI", bir programın, belirli bir programa yönelik bir CPI gerçekleştirmekten ziyade, talimat işleyicisine geçirilen herhangi bir programa CPI vermek üzere yapılandırıldığı anlamına gelir. Programınızın talimat işleyicisinin çağrıcıları, talimatların hesaplar listesine istedikleri herhangi bir programı geçirebileceğinden, geçirilen bir programın adresini doğrulamamak, programınızın rastgele programlara CPI'ler gerçekleştirmesine neden olur.
Unutmayın, program kontrollerinin eksikliği kötü niyetli kullanıcılar için fırsatlar yaratabilir.
Bu program kontrol eksikliği, kötü niyetli bir kullanıcının beklenmeyen bir programı geçirebileceği bir fırsat yaratır, bu da orijinal programın bu gizemli programa bir talimat işleyicisini çağırmasına neden olur. Bu CPI'nin sonuçlarının ne olacağını söylemek imkânsızdır. Bu, hem orijinal programın hem de beklenmedik programın mantığına, ayrıca orijinal talimat işleyicisine geçirilen diğer hesapların ne olduğuna bağlıdır.
Eksik Program Kontrolleri
Aşağıdaki programı bir örnek olarak alın. cpi
talimat işleyicisi token_program
üzerinde transfer
talimat işleyicisini çağırıyor, ancak token_program
ın talimat işleyicisine geçirilen hesap olup olmadığını kontrol eden bir kod yok.
use anchor_lang::prelude::*;
use anchor_lang::solana_program;
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
#[program]
pub mod arbitrary_cpi_insecure {
use super::*;
pub fn cpi(ctx: Context<Cpi>, amount: u64) -> ProgramResult {
solana_program::program::invoke(
&spl_token::instruction::transfer(
ctx.accounts.token_program.key,
ctx.accounts.source.key,
ctx.accounts.destination.key,
ctx.accounts.authority.key,
&[],
amount,
)?,
&[
ctx.accounts.source.clone(),
ctx.accounts.destination.clone(),
ctx.accounts.authority.clone(),
],
)
}
}
#[derive(Accounts)]
pub struct Cpi<'info> {
source: UncheckedAccount<'info>,
destination: UncheckedAccount<'info>,
authority: UncheckedAccount<'info>,
token_program: UncheckedAccount<'info>,
}
Bir saldırgan, bu talimat işleyicisini kolayca çağırabilir ve kontrol ettiği bir kopya token programını geçirebilir.
Program Kontrollerini Ekle
Bu zafiyeti düzeltmek için, token_program
ın genel anahtarının SPL Token Programı olup olmadığını kontrol etmek üzere cpi
talimat işleyicisine birkaç satır eklemek mümkündür.
pub fn cpi_secure(ctx: Context<Cpi>, amount: u64) -> ProgramResult {
if &spl_token::ID != ctx.accounts.token_program.key {
return Err(ProgramError::IncorrectProgramId);
}
solana_program::program::invoke(
&spl_token::instruction::transfer(
ctx.accounts.token_program.key,
ctx.accounts.source.key,
ctx.accounts.destination.key,
ctx.accounts.authority.key,
&[],
amount,
)?,
&[
ctx.accounts.source.clone(),
ctx.accounts.destination.clone(),
ctx.accounts.authority.clone(),
],
)
}
Artık, bir saldırgan farklı bir token programı geçirirse, talimat işleyici ProgramError::IncorrectProgramId
hatasını döndürecektir.
CPI ile çağırdığınız programın program kimliğinin adresini sabit kodlayabilir veya kullanılabilir ise programın Rust kütüphanesini kullanarak programın adresini alabilirsiniz. Yukarıdaki örnekte, spl_token
kütüphanesi SPL Token Programı'nın adresini sağlar.
Anchor CPI Modülünü Kullan
Program kontrollerini yönetmenin daha basit bir yolu, Anchor CPI modülünü kullanmaktır. Daha önceki Anchor CPI dersi
boyunca, Anchor'ın programların CPI'lerini daha basit hale getirmek için otomatik olarak CPI modülleri oluşturduğunu öğrendik. Bu modüller ayrıca, bir kamu talimatına geçirilen programın genel anahtarını doğrulayarak güvenliği artırır.
Her Anchor programı, programın adresini tanımlamak için declare_id()
makrosunu kullanır. Belirli bir program için bir CPI modülü oluşturulduğunda, bu makroya geçirilen adres "kesin bilgi kaynağı" olarak kullanılır ve otomatik olarak, CPI modülünü kullanan tüm CPI'lerin bu program kimliğini hedeflemesini doğrular.
Temelde manuel program kontrollerinden farklı olmamakla birlikte, CPI modüllerini kullanmak, bir program kontrolünü gerçekleştirmeyi unutmamak veya sabit kodlama sırasında yanlış program kimliğini yazma olasılığını ortadan kaldırır.
Kullanım Örneği
Aşağıdaki program, SPL Token Programı için, önceki örneklerde gösterilen transfer işlemini gerçekleştirmek üzere bir CPI modülünün kullanımına bir örnek gösterir.
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount};
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
#[program]
pub mod arbitrary_cpi_recommended {
use super::*;
pub fn cpi(ctx: Context<Cpi>, amount: u64) -> ProgramResult {
token::transfer(ctx.accounts.transfer_ctx(), amount)
}
}
#[derive(Accounts)]
pub struct Cpi<'info> {
source: Account<'info, TokenAccount>,
destination: Account<'info, TokenAccount>,
authority: Signer<'info>,
token_program: Program<'info, Token>,
}
impl<'info> Cpi<'info> {
pub fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> {
let program = self.token_program.to_account_info();
let accounts = token::Transfer {
from: self.source.to_account_info(),
to: self.destination.to_account_info(),
authority: self.authority.to_account_info(),
};
CpiContext::new(program, accounts)
}
}
Ek Bilgi
Yukarıdaki örnekte olduğu gibi, Anchor, CPI'leri onlarla çalışıyormuş gibi çıkarmanızı sağlayan birkaç popüler yerel program için sarmalayıcılar oluşturdu.
Ayrıca, CPI yaptığınız program türüne bağlı olarak, belki de Anchor'ın
Program
hesap türünü
kullanarak hesap doğrulama yapabilirsiniz. Hem anchor_lang
hem de anchor_spl
kütüphanelerinde, kutudan gelen aşağıdaki Program
türleri sağlanır:
Bir Anchor programının CPI modülüne erişiminiz varsa, genellikle programın türünü aşağıdaki gibi içe aktarabilirsiniz; program adını gerçek programın adıyla değiştirmeyi unutmayın:
use other_program::program::OtherProgram;
Laboratuvar
CPI'ler için kullandığınız programla kontroller yapmanın önemini göstermek için, basitleştirilmiş ve biraz kurgusal bir oyun üzerinde çalışacağız. Bu oyun, PDA hesaplarıyla karakterleri temsil eder ve karakterlerin sağlık ve güç gibi meta verilerini ve özelliklerini yönetmek için ayrı bir "meta veri" programı kullanır.
Bu örnek biraz kurgusal olsa da, aslında Solana'daki NFT'lerin nasıl çalıştığına çok benzer bir mimariye sahiptir: SPL Token Programı token minlerini, dağıtımını ve transferlerini yönetir ve ayrı bir meta veri programı token'lara meta verileri atamak için kullanılır. Bu yüzden burada geçirdiğimiz zafiyet, gerçek token'lara da uygulanabilir.
1. Kurulum
Bu depo için
starter
dalı ile başlayacağız.
Depoyu klonlayın ve ardından starter
dalında açın.
Üç program olduğunu göreceksiniz:
gameplay
character-metadata
fake-metadata
İlk program, gameplay
, doğrudan testimizin kullandığı programdır. Programa göz atın. İki talimatı vardır:
create_character_insecure
- yeni bir karakter oluşturur ve meta veri programına karakterin başlangıç özelliklerini kurmak için CPI yaparbattle_insecure
- iki karakteri birbirine karşı karşıya getirir ve en yüksek özelliklere sahip karaktere "zafer" atar
İpucu: Geçerli bir program kimliği doğruladığınızdan emin olun. Bu, programınızın güvenliğini artırmak için önemlidir.
İkinci program character-metadata
, karakter meta verilerini işlemek için "onaylı" program olarak tasarlanmıştır. Bu programa göz atın. Yeni bir PDA oluşturan create_metadata
için tek bir talimat işleyicisi vardır ve karakterin sağlık ve gücü için 0 ile 20 arasında bir değeri rastgele atar.
Son program fake-metadata
, gameplay
programını kötüye kullanmak için bir saldırganın oluşturabileceği "sahte" bir meta veri programıdır. Bu program, character-metadata
programına neredeyse tamamen benzerdir; tek fark, karakterin başlangıç sağlık ve gücünü maksimum izin verilen değer olan 255 olarak ayarlamasıdır.
2. create_character_insecure
Talimat İşleyicisini Test Et
Bunun için tests
dizininde zaten bir test var. Uzun ama birlikte incelemeden önce bir dakikanızı ayırın:
it("Güvensiz talimatlar saldırganın her seferinde başarıyla kazanmasına izin veriyor", async () => {
try {
// Gerçek meta veri programı ile oyuncu birini başlat
await gameplayProgram.methods
.createCharacterInsecure()
.accounts({
metadataProgram: metadataProgram.programId,
authority: playerOne.publicKey,
})
.signers([playerOne])
.rpc();
// Saldırganı sahte meta veri programı ile başlat
await gameplayProgram.methods
.createCharacterInsecure()
.accounts({
metadataProgram: fakeMetadataProgram.programId,
authority: attacker.publicKey,
})
.signers([attacker])
.rpc();
// Her iki oyuncunun meta veri hesaplarını al
const [playerOneMetadataKey] = getMetadataKey(
playerOne.publicKey,
gameplayProgram.programId,
metadataProgram.programId,
);
const [attackerMetadataKey] = getMetadataKey(
attacker.publicKey,
gameplayProgram.programId,
fakeMetadataProgram.programId,
);
const playerOneMetadata =
await metadataProgram.account.metadata.fetch(playerOneMetadataKey);
const attackerMetadata =
await fakeMetadataProgram.account.metadata.fetch(attackerMetadataKey);
// Normal oyuncunun sağlık ve gücü 0 ile 20 arasında olmalı
expect(playerOneMetadata.health).to.be.lessThan(20);
expect(playerOneMetadata.power).to.be.lessThan(20);
// Saldırganın sağlık ve gücü 255 olacak
expect(attackerMetadata.health).to.equal(255);
expect(attackerMetadata.power).to.equal(255);
} catch (error) {
console.error("Test hatalı:", error);
throw error;
}
});
Bu test, normal bir oyuncu ve bir saldırganın her ikisinin de karakterlerini nasıl oluşturduğunu gösteriyor. Sadece saldırgan, sahte meta veri programının program kimliğini geçiriyor, bu da create_character_insecure
talimatının program kontrollerine sahip olmaması nedeniyle çalışmasına fırsat tanıyor.
Sonuç olarak, normal karakter uygun sağlık ve güç değerlerine sahip: her biri 0 ile 20 arasında bir değer. Ancak saldırganın sağlık ve gücü her biri 255, bu da saldırganı yenilmez kılıyor.
Henüz yapmadıysanız, anchor test
komutunu çalıştırın ve bu testin gerçekten tanımlandığı gibi davrandığını görün.
3. create_character_secure
Talimat İşleyicisini Oluştur
Bunu, yeni bir karakter oluşturmak için güvenli bir talimat işleyicisi oluşturarak düzeltelim. Bu talimat işleyicisi, uygun program kontrollerini uygulamalı ve CPI'yi yapmak için character-metadata
programının cpi
kütüphanesini kullanmalıdır; sadece invoke
kullanmamalıdır.
Kendi yeteneklerinizi test etmek isterseniz, devam etmeden önce bunu kendiniz deneyin.
Öncelikle, gameplay
programının lib.rs
dosyasının en üstünde use
ifademizi güncelleyelim. Hesap doğrulama için program türüne ve create_metadata
CPI'sini gerçekleştirmek için yardımcı işleve erişim sağlayacağız.
use character_metadata::{
cpi::accounts::CreateMetadata,
cpi::create_metadata,
program::CharacterMetadata,
};
Ardından, CreateCharacterSecure
adında yeni bir hesap doğrulama yapısı oluşturalım. Bu sefer, metadata_program
bir Program
türü oluyor:
#[derive(Accounts)]
pub struct CreateCharacterSecure<'info> {
#[account(mut)]
pub authority: Signer<'info>,
#[account(
init,
payer = authority,
space = DISCRIMINATOR_SIZE + Character::INIT_SPACE,
seeds = [authority.key().as_ref()],
bump
)]
pub character: Account<'info, Character>,
#[account(
mut,
seeds = [character.key().as_ref()],
seeds::program = metadata_program.key(),
bump,
)]
/// CHECK: Bu hesap anchor tarafından kontrol edilmeyecek
pub metadata_account: AccountInfo<'info>,
pub metadata_program: Program<'info, CharacterMetadata>,
pub system_program: Program<'info, System>,
}
Son olarak, create_character_secure
talimat işleyicisini ekleyelim. Daha önceki ile aynı olacak, ancak artık doğrudan invoke
yerine Anchor CPIs'in tam işlevselliğini kullanacak:
pub fn create_character_secure(ctx: Context<CreateCharacterSecure>) -> Result<()> {
// Karakter verilerini başlat
let character = &mut ctx.accounts.character;
character.metadata = ctx.accounts.metadata_account.key();
character.authority = ctx.accounts.authority.key();
character.wins = 0;
// CPI bağlamını hazırla
let cpi_context = CpiContext::new(
ctx.accounts.metadata_program.to_account_info(),
CreateMetadata {
character: ctx.accounts.character.to_account_info(),
metadata: ctx.accounts.metadata_account.to_owned(),
authority: ctx.accounts.authority.to_account_info(),
system_program: ctx.accounts.system_program.to_account_info(),
},
);
// Meta verileri oluşturmak için CPI'yi gerçekleştir
create_metadata(cpi_context)?;
Ok(())
}
4. create_character_secure
Talimat İşleyicisini Test Et
Artık yeni bir karakteri güvenli bir şekilde başlatmanın bir yolunu bulduğumuza göre, yeni bir test oluşturalım. Bu test, sadece saldırganın karakterini başlatmaya çalışmalı ve bir hata fırlatılmasını beklemelidir.
it("sahte program ile güvenli karakter oluşturmayı engeller", async () => {
try {
await gameplayProgram.methods
.createCharacterSecure()
.accounts({
metadataProgram: fakeMetadataProgram.programId,
authority: attacker.publicKey,
})
.signers([attacker])
.rpc();
throw new Error("createCharacterSecure'ın bir hata fırlatmasını bekliyordum");
} catch (error) {
expect(error).to.be.instanceOf(Error);
console.log(error);
}
});
Henüz yapmadıysanız, anchor test
komutunu çalıştırın. Beklendiği gibi bir hatanın fırlatıldığını fark edin; talimat işleyicisine geçirilen program kimliğinin beklenen program kimliğiyle uyuşmadığını gösteren ayrıntılı bir açıklama:
'Program log: AnchorError caused by account: metadata_program. Error Code: InvalidProgramId. Error Number: 3008. Error Message: Program ID was not as expected.',
'Program log: Left:',
'Program log: HQqG7PxftCD5BB9WUWcYksrjDLUwCmbV8Smh1W8CEgQm',
'Program log: Right:',
'Program log: 4FgVd2dgsFnXbSHz8fj9twNbfx8KWcBJkHa6APicU6KS'
Rastgele CPI'lere karşı korunmanın yolu budur!
Bazı durumlarda, programınızın CPIs'inde daha fazla esneklik istemek isteyebilirsiniz. İhtiyacınız olan programı tasarlamanızda sizi engellemeyeceğiz, ancak programınızda herhangi bir güvenlik açığı olmadığından emin olmak için mümkün olan her önlemi almanızı rica ediyoruz.
Son çözüm koduna göz atmak isterseniz, bunu aynı deponun solution
dalında bulabilirsiniz.
Zorluk
Bu birim içindeki diğer derslerde olduğu gibi, bu güvenlik açığını önlemek için fırsatınız kendi veya diğer programları denetlemektir.
En az bir programı gözden geçirmek için biraz zaman ayırın ve talimat işleyicilerine geçirilen her program için program kontrollerinin yerinde olduğundan emin olun; özellikle CPI ile çağrılan programlar için.
Başka birinin programında bir hata veya açığı bulursanız, lütfen onlara bildirin! Kendi programınızda bir tane bulursanız, hemen düzeltildiğinden emin olun.
Kodunuzu GitHub'a gönderin ve bize bu ders hakkında ne düşündüğünüzü söyleyin!